use crate::{
    state::State,
    context::Context,
    texture::Texture,
    uniform::Uniform,
    camera::{Camera, CameraUniform},
    control::CameraControl
};

use super::scene::{Scene, SimpleChunk};

pub struct Renderer {
    ctx: Context,
    depth_texture_view: wgpu::TextureView,
    chunk: SimpleChunk,
    i: usize,
    scene: Scene,
    scene_instance_buffer: wgpu::Buffer,
    camera: Camera,
    camera_uniform: Uniform<CameraUniform>,
    render_pipeline: wgpu::RenderPipeline,
    camera_control: CameraControl
}

impl Renderer {
    pub async fn new(window: winit::window::Window) -> Self {
        let ctx = Context::new(window).await;
        let Context { device, config, .. } = &ctx;

        let depth_texture_view = Texture::new_depth_texture(
            device,
            config.width,
            config.height
        );

        let mut chunk = SimpleChunk::new();
        chunk.update(&[
            ([1, 1, 1], 1),
            ([1, 1, 2], 1),
            ([2, 1, 1], 1),
            ([2, 1, 2], 1)
        ]);
        let mut scene = Scene::new();
        scene.update(&chunk);
        let scene_instance_buffer = scene.create_buffer(device);

        let camera_bind_group_layout = Uniform::bind_group_layout(
            device,
            CameraUniform::VISIBILITY,
            "Camera Uniform Bind Group Layout"
        );
        let camera = Camera {
            pos: (0.0, 0.0, 5.0).into(),
            up: cgmath::Vector3::unit_y(),
            yaw: cgmath::Deg(-90.0),
            pitch: cgmath::Deg(0.0),
            speed: 1.0,
            rot_speed: cgmath::Deg(70.0),
            fovy: cgmath::Deg(45.0).into(),
            aspect: config.width as f32 / config.height as f32,
            near: 0.1,
            far: 100.0
        };
        let camera_uniform = Uniform::new(
            device,
            &camera_bind_group_layout,
            &CameraUniform::new(&camera),
            "Camera Uniform Buffer",
            "Camera Uniform Bind Group"
        );

        let shader = device.create_shader_module(wgpu::include_wgsl!("shader.wgsl"));
        let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("Voxel Render Pipeline Layout"),
            bind_group_layouts: &[&camera_bind_group_layout],
            push_constant_ranges: &[]
        });
        let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("Voxel Render Pipeline"),
            layout: Some(&render_pipeline_layout),
            vertex: wgpu::VertexState {
                module: &shader,
                entry_point: "vs_main",
                buffers: &[Scene::buffer_layout::<0>()]
            },
            fragment: Some(wgpu::FragmentState {
                module: &shader,
                entry_point: "fs_main",
                targets: &[Some(wgpu::ColorTargetState {
                    format: config.format,
                    blend: Some(wgpu::BlendState::REPLACE),
                    write_mask: wgpu::ColorWrites::ALL
                })]
            }),
            primitive: wgpu::PrimitiveState {
                topology: wgpu::PrimitiveTopology::TriangleList,
                strip_index_format: None,
                front_face: wgpu::FrontFace::Ccw,
                cull_mode: Some(wgpu::Face::Back),
                polygon_mode: wgpu::PolygonMode::Fill,
                unclipped_depth: false,
                conservative: false
            },
            depth_stencil: Some(wgpu::DepthStencilState {
                format: Texture::DEPTH_FORMAT,
                depth_write_enabled: true,
                depth_compare: wgpu::CompareFunction::Less,
                stencil: Default::default(),
                bias: Default::default()
            }),
            multisample: wgpu::MultisampleState {
                count: 1,
                mask: !0,
                alpha_to_coverage_enabled: false
            },
            multiview: None
        });

        let camera_control = CameraControl::new();

        Self {
            ctx,
            depth_texture_view,
            chunk,
            i: 2,
            scene,
            scene_instance_buffer,
            camera,
            camera_uniform,
            render_pipeline,
            camera_control
        }
    }
}

impl State for Renderer {
    fn get_window(&self) -> &winit::window::Window {
        &self.ctx.window
    }

    fn reconfigure(&self) {
        self.ctx.reconfigure();
    }

    fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
        self.ctx.resize(new_size);

        let config = &self.ctx.config;

        self.depth_texture_view = Texture::new_depth_texture(
            &self.ctx.device,
            config.width,
            config.height
        );

        self.camera.aspect = config.width as f32 / config.height as f32;
    }

    fn input(&mut self, event: &winit::event::WindowEvent) -> bool {
        if let winit::event::WindowEvent::KeyboardInput {
            input: winit::event::KeyboardInput {
                state: winit::event::ElementState::Released,
                virtual_keycode: Some(code),
                ..
            },
            ..
        } = event {
            match code {
                winit::event::VirtualKeyCode::Key1 => {
                    if self.i > 10 { return true; }
                    self.chunk.update(&[
                        ([1, self.i, 1], 1),
                        ([1, self.i, 2], 1),
                        ([2, self.i, 1], 1),
                        ([2, self.i, 2], 1)
                    ]);
                    self.scene.update(&self.chunk);
                    self.scene_instance_buffer = self.scene.create_buffer(&self.ctx.device);
                    self.i += 1;
                    return true;
                }
                winit::event::VirtualKeyCode::Key2 => {
                    if self.i <= 2 { return true; }
                    self.i -= 1;
                    self.chunk.update(&[
                        ([1, self.i, 1], 0),
                        ([1, self.i, 2], 0),
                        ([2, self.i, 1], 0),
                        ([2, self.i, 2], 0)
                    ]);
                    self.scene.update(&self.chunk);
                    self.scene_instance_buffer = self.scene.create_buffer(&self.ctx.device);
                    return true;
                }
                _ => {}
            }
        }

        self.camera_control.handle_events(event)
    }

    fn update(&mut self) {
        self.camera_control.update_camera(&mut self.camera);
        self.camera_uniform.update(&self.ctx.queue, &CameraUniform::new(&self.camera));
    }

    fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
        let output = self.ctx.surface.get_current_texture()?;
        let view = output.texture.create_view(&Default::default());
        let mut encoder = self.ctx.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
            label: Some("Voxel Render Encoder")
        });

        let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
            label: Some("Voxel Render Pass"),
            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                view: &view,
                resolve_target: None,
                ops: wgpu::Operations {
                    load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.1, g: 0.2, b: 0.3, a: 1.0 }),
                    store: true
                }
            })],
            depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
                view: &self.depth_texture_view,
                depth_ops: Some(wgpu::Operations {
                    load: wgpu::LoadOp::Clear(1.0),
                    store: true
                }),
                stencil_ops: None
            })
        });
        render_pass.set_pipeline(&self.render_pipeline);
        render_pass.set_bind_group(0, &self.camera_uniform.bind_group, &[]);
        render_pass.set_vertex_buffer(0, self.scene_instance_buffer.slice(..));
        render_pass.draw(0..6, 0..self.scene.len() as u32);
        drop(render_pass);

        self.ctx.queue.submit(std::iter::once(encoder.finish()));
        output.present();

        Ok(())
    }
}
